using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
using JetBrains.Annotations;
using Kaleida.ServiceMonitor.Framework;
using Kaleida.ServiceMonitor.Model.StructuredSource;

namespace Kaleida.ServiceMonitor.Model.Runtime
{
    public class DirectiveProcessor
    {
        private readonly IList<Type> directiveTypes;
        private readonly IList<Assembly> searchAssemblies;

        public DirectiveProcessor()
        {
            searchAssemblies = new[]{Assembly.GetExecutingAssembly()};
            directiveTypes = GetDirectiveTypes();
        }

        private static IScriptDirective CreateInstance(Type type, IEnumerable<string> arguments)
        {
            try
            {
                return (IScriptDirective) Activator.CreateInstance(type, arguments.Select(i=>(object)i).ToArray());
            }
            catch (TargetInvocationException exception)
            {
                throw exception.InnerException;
            }
        }

        public CompiledNode Apply([NotNull] CompiledScriptModule scriptModule, IEnumerable<DirectiveDefinition> directiveDefinitions)
        {
            if (scriptModule == null) throw new ArgumentNullException("scriptModule");

            var node = new CompiledNode(scriptModule);

            foreach (var definition in directiveDefinitions)
            {
                var directiveType = GetDirectiveType(definition);
                var directive = CreateInstance(directiveType, definition.Arguments);

                if(node.IsLeaf)
                    node = directive.Apply(node.ScriptModule);
            }

            return node;
        }

        private Type GetDirectiveType(DirectiveDefinition directiveDefinition)
        {
            var dotNetClassName = directiveDefinition.Text.ToPascalCase() + "Directive";
            var argumentCount = directiveDefinition.Arguments.Count();

            var withMatchingName = new HashSet<Type>(directiveTypes.Where(i => i.Name == dotNetClassName));
            var withMatchingConstructor = new HashSet<Type>(directiveTypes.Where(i => i.HasPublicFullyStringParameterisedConstructor(argumentCount)));

            var candidateTypes = withMatchingName.Intersect(withMatchingConstructor);
            var directiveType = candidateTypes.FirstOrDefault();

            if (directiveType == null)
            {
                var message = new StringBuilder();
                message.AppendFormat("Cannot find a IScriptDirective implementation for '{0}' within these search assemblies:\r\n", directiveDefinition.Text);
                message.Append(searchAssemblies.Select(i => i.FullName).ToBulletedList());
                message.Append("\r\n\r\n");
                message.AppendFormat("The .NET implementation must be named '{0}', ", dotNetClassName);
                message.AppendFormat("derive from {0}, ", typeof (IScriptDirective).Name);
                message.AppendFormat("have public visibility and a public constructor containing all string parameters.\r\n\r\n");

                throw new InvalidOperationException(message.ToString());
            }
            return directiveType;
        }

        private IList<Type> GetDirectiveTypes()
        {
            var searchTypes = searchAssemblies.SelectMany(i => i.GetTypes()).ToList();

            var withCorrectBase = new HashSet<Type>(searchTypes.Where(i => i.DerivesFrom(typeof(IScriptDirective))));
            var withPublicVisibility = new HashSet<Type>(searchTypes.Where(i => i.IsVisible));
            var withMatchingConstructor = new HashSet<Type>(searchTypes.Where(i => i.HasPublicFullyStringParameterisedConstructor()));

            return withCorrectBase.Intersect(withPublicVisibility).Intersect(withMatchingConstructor).ToList();
        }
    }
}